Bahasa Indonesia

Kuasai React Suspense untuk pengambilan data. Pelajari cara mengelola status memuat secara deklaratif, tingkatkan UX dengan transisi, dan tangani error dengan Batas Error.

Batas Suspense React: Selami Lebih Dalam Manajemen Status Memuat Deklaratif

Dalam dunia pengembangan web modern, menciptakan pengalaman pengguna yang mulus dan responsif adalah hal yang terpenting. Salah satu tantangan paling persisten yang dihadapi pengembang adalah mengelola status memuat (loading states). Mulai dari mengambil data untuk profil pengguna hingga memuat bagian baru dari aplikasi, momen menunggu sangatlah krusial. Secara historis, ini melibatkan jalinan bendera boolean yang rumit seperti isLoading, isFetching, dan hasError, yang tersebar di seluruh komponen kita. Pendekatan imperatif ini mengotori kode kita, memperumit logika, dan sering menjadi sumber bug, seperti kondisi balapan (race conditions).

Masuklah React Suspense. Awalnya diperkenalkan untuk pemisahan kode (code-splitting) dengan React.lazy(), kemampuannya telah berkembang secara dramatis dengan React 18 menjadi mekanisme yang kuat dan kelas utama untuk menangani operasi asinkron, terutama pengambilan data. Suspense memungkinkan kita untuk mengelola status memuat secara deklaratif, yang secara fundamental mengubah cara kita menulis dan berpikir tentang komponen kita. Alih-alih bertanya "Apakah saya sedang memuat?", komponen kita cukup berkata, "Saya butuh data ini untuk dirender. Selama saya menunggu, tolong tampilkan UI fallback ini."

Panduan komprehensif ini akan membawa Anda dalam perjalanan dari metode manajemen state tradisional ke paradigma deklaratif React Suspense. Kita akan menjelajahi apa itu batas Suspense, bagaimana cara kerjanya untuk pemisahan kode dan pengambilan data, dan bagaimana mengatur UI pemuatan yang kompleks yang memanjakan pengguna Anda alih-alih membuat mereka frustrasi.

Cara Lama: Repotnya Mengelola Status Memuat Manual

Sebelum kita dapat sepenuhnya mengapresiasi keanggunan Suspense, penting untuk memahami masalah yang dipecahkannya. Mari kita lihat komponen khas yang mengambil data menggunakan hook useEffect dan useState.

Bayangkan sebuah komponen yang perlu mengambil dan menampilkan data pengguna:


import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Reset state untuk userId baru
    setIsLoading(true);
    setUser(null);
    setError(null);

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Respons jaringan tidak baik');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Ambil ulang data saat userId berubah

  if (isLoading) {
    return <p>Memuat profil...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

Pola ini fungsional, tetapi memiliki beberapa kelemahan:

Memperkenalkan React Suspense: Pergeseran Paradigma

Suspense membalik model ini. Alih-alih komponen mengelola status memuat secara internal, ia mengomunikasikan ketergantungannya pada operasi asinkron langsung ke React. Jika data yang dibutuhkannya belum tersedia, komponen akan "menangguhkan" (suspend) rendering.

Ketika sebuah komponen melakukan suspend, React akan berjalan ke atas pohon komponen untuk menemukan Batas Suspense terdekat. Batas Suspense adalah komponen yang Anda definisikan di pohon Anda menggunakan <Suspense>. Batas ini kemudian akan merender UI fallback (seperti spinner atau skeleton loader) sampai semua komponen di dalamnya telah menyelesaikan dependensi data mereka.

Ide intinya adalah menempatkan dependensi data bersama dengan komponen yang membutuhkannya, sambil memusatkan UI pemuatan di tingkat yang lebih tinggi di pohon komponen. Ini membersihkan logika komponen dan memberi Anda kontrol yang kuat atas pengalaman memuat pengguna.

Bagaimana Sebuah Komponen Melakukan "Suspend"?

Keajaiban di balik Suspense terletak pada sebuah pola yang mungkin tampak tidak biasa pada awalnya: melempar sebuah Promise. Sumber data yang mendukung Suspense bekerja seperti ini:

  1. Ketika sebuah komponen meminta data, sumber data memeriksa apakah ia memiliki data yang di-cache.
  2. Jika data tersedia, ia mengembalikannya secara sinkron.
  3. Jika data tidak tersedia (yaitu, sedang diambil), sumber data melempar Promise yang mewakili permintaan pengambilan yang sedang berlangsung.

React menangkap Promise yang dilempar ini. Ini tidak membuat aplikasi Anda crash. Sebaliknya, ia menafsirkannya sebagai sinyal: "Komponen ini belum siap untuk dirender. Jeda, dan cari batas Suspense di atasnya untuk menampilkan fallback." Begitu Promise terselesaikan, React akan mencoba kembali merender komponen, yang sekarang akan menerima datanya dan berhasil dirender.

Batas <Suspense>: Deklarator UI Memuat Anda

Komponen <Suspense> adalah jantung dari pola ini. Sangat mudah digunakan, hanya membutuhkan satu prop wajib: fallback.


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>Aplikasi Saya</h1>
      <Suspense fallback={<p>Memuat konten...</p>}>
        <SomeComponentThatFetchesData />
      </Suspense>
    </div>
  );
}

Dalam contoh ini, jika SomeComponentThatFetchesData melakukan suspend, pengguna akan melihat pesan "Memuat konten..." sampai datanya siap. Fallback bisa berupa node React yang valid, dari string sederhana hingga komponen skeleton yang kompleks.

Kasus Penggunaan Klasik: Pemisahan Kode (Code Splitting) dengan React.lazy()

Penggunaan Suspense yang paling mapan adalah untuk pemisahan kode. Ini memungkinkan Anda untuk menunda pemuatan JavaScript untuk sebuah komponen sampai benar-benar dibutuhkan.


import React, { Suspense, lazy } from 'react';

// Kode komponen ini tidak akan ada di bundel awal.
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Beberapa konten yang dimuat segera</h2>
      <Suspense fallback={<div>Memuat komponen...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Di sini, React hanya akan mengambil JavaScript untuk HeavyComponent ketika pertama kali mencoba merendernya. Selama sedang diambil dan di-parsing, fallback Suspense akan ditampilkan. Ini adalah teknik yang kuat untuk meningkatkan waktu muat halaman awal.

Garis Depan Modern: Pengambilan Data dengan Suspense

Meskipun React menyediakan mekanisme Suspense, ia tidak menyediakan klien pengambilan data tertentu. Untuk menggunakan Suspense untuk pengambilan data, Anda memerlukan sumber data yang terintegrasi dengannya (yaitu, yang melempar Promise saat data tertunda).

Kerangka kerja seperti Relay dan Next.js memiliki dukungan bawaan kelas utama untuk Suspense. Pustaka pengambilan data populer seperti TanStack Query (sebelumnya React Query) dan SWR juga menawarkan dukungan eksperimental atau penuh untuk itu.

Untuk memahami konsepnya, mari kita buat pembungkus yang sangat sederhana dan konseptual di sekitar API fetch untuk membuatnya kompatibel dengan Suspense. Catatan: Ini adalah contoh yang disederhanakan untuk tujuan pendidikan dan tidak siap produksi. Ini kekurangan caching yang tepat dan seluk-beluk penanganan error.


// data-fetcher.js
// Cache sederhana untuk menyimpan hasil
const cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
  }

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // Inilah keajaibannya!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Pengambilan gagal dengan status ${response.status}`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

Pembungkus ini menjaga status sederhana untuk setiap URL. Ketika fetchData dipanggil, ia memeriksa status. Jika tertunda, ia melempar promise. Jika berhasil, ia mengembalikan data. Sekarang, mari kita tulis ulang komponen UserProfile kita menggunakan ini.


// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';

// Komponen yang benar-benar menggunakan data
function ProfileDetails({ userId }) {
  // Coba baca data. Jika belum siap, ini akan melakukan suspend.
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Komponen induk yang mendefinisikan UI status memuat
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Memuat profil...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

Lihat perbedaannya! Komponen ProfileDetails bersih dan hanya berfokus pada rendering data. Ia tidak memiliki state isLoading atau error. Ia hanya meminta data yang dibutuhkannya. Tanggung jawab untuk menampilkan indikator pemuatan telah dipindahkan ke komponen induk, UserProfile, yang secara deklaratif menyatakan apa yang harus ditampilkan saat menunggu.

Mengatur Status Memuat yang Kompleks

Kekuatan sejati Suspense menjadi nyata ketika Anda membangun UI kompleks dengan banyak dependensi asinkron.

Batas Suspense Bersarang untuk UI Bertahap

Anda dapat menyarangkan batas Suspense untuk menciptakan pengalaman memuat yang lebih halus. Bayangkan halaman dasbor dengan sidebar, area konten utama, dan daftar aktivitas terkini. Masing-masing mungkin memerlukan pengambilan datanya sendiri.


function DashboardPage() {
  return (
    <div>
      <h1>Dasbor</h1>
      <div className="layout">
        <Suspense fallback={<p>Memuat navigasi...</p>}>
          <Sidebar />
        </Suspense>

        <main>
          <Suspense fallback={<ProfileSkeleton />}>
            <MainContent />
          </Suspense>

          <Suspense fallback={<ActivityFeedSkeleton />}>
            <ActivityFeed />
          </Suspense>
        </main>
      </div>
    </div>
  );
}

Dengan struktur ini:

Ini memungkinkan Anda untuk menunjukkan konten yang berguna kepada pengguna secepat mungkin, secara dramatis meningkatkan kinerja yang dirasakan.

Menghindari UI "Popcorning"

Terkadang, pendekatan bertahap dapat menyebabkan efek yang mengganggu di mana beberapa spinner muncul dan menghilang secara berurutan, efek yang sering disebut "popcorning." Untuk mengatasi ini, Anda dapat memindahkan batas Suspense lebih tinggi di pohon komponen.


function DashboardPage() {
  return (
    <div>
      <h1>Dasbor</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <div className="layout">
          <Sidebar />
          <main>
            <MainContent />
            <ActivityFeed />
          </main>
        </div>
      </Suspense>
    </div>
  );
}

Dalam versi ini, satu DashboardSkeleton ditampilkan sampai semua komponen anak (Sidebar, MainContent, ActivityFeed) memiliki data yang siap. Seluruh dasbor kemudian muncul sekaligus. Pilihan antara batas bersarang dan satu batas tingkat lebih tinggi adalah keputusan desain UX yang dibuat Suspense menjadi sepele untuk diimplementasikan.

Penanganan Error dengan Batas Error (Error Boundaries)

Suspense menangani status tertunda (pending) dari sebuah promise, tetapi bagaimana dengan status ditolak (rejected)? Jika promise yang dilempar oleh komponen ditolak (misalnya, error jaringan), itu akan diperlakukan seperti error rendering lainnya di React.

Solusinya adalah menggunakan Batas Error (Error Boundaries). Batas Error adalah komponen kelas yang mendefinisikan metode siklus hidup khusus, componentDidCatch() atau metode statis getDerivedStateFromError(). Ia menangkap error JavaScript di mana saja di pohon komponen anaknya, mencatat error tersebut, dan menampilkan UI fallback.

Berikut adalah komponen Batas Error yang sederhana:


import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Perbarui state agar render berikutnya akan menampilkan UI fallback.
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    // Anda juga dapat mencatat error ke layanan pelaporan error
    console.error("Menangkap sebuah error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Anda dapat merender UI fallback kustom apa pun
      return <h1>Terjadi kesalahan. Silakan coba lagi.</h1>;
    }

    return this.props.children; 
  }
}

Anda kemudian dapat menggabungkan Batas Error dengan Suspense untuk menciptakan sistem yang kuat yang menangani ketiga status: tertunda, berhasil, dan error.


import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';

function App() {
  return (
    <div>
      <h2>Informasi Pengguna</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Memuat...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Dengan pola ini, jika pengambilan data di dalam UserProfile berhasil, profil ditampilkan. Jika tertunda, fallback Suspense ditampilkan. Jika gagal, fallback Batas Error ditampilkan. Logikanya deklaratif, komposisional, dan mudah dipahami.

Transisi: Kunci Pembaruan UI Tanpa Blokir

Ada satu bagian terakhir dari teka-teki ini. Pertimbangkan interaksi pengguna yang memicu pengambilan data baru, seperti mengklik tombol "Berikutnya" untuk melihat profil pengguna yang berbeda. Dengan pengaturan di atas, saat tombol diklik dan prop userId berubah, komponen UserProfile akan melakukan suspend lagi. Ini berarti profil yang sedang terlihat akan hilang dan digantikan oleh fallback pemuatan. Ini bisa terasa mendadak dan mengganggu.

Di sinilah transisi berperan. Transisi adalah fitur baru di React 18 yang memungkinkan Anda menandai pembaruan state tertentu sebagai tidak mendesak. Ketika pembaruan state dibungkus dalam transisi, React akan terus menampilkan UI lama (konten basi) sementara ia menyiapkan konten baru di latar belakang. Ia hanya akan melakukan pembaruan UI setelah konten baru siap ditampilkan.

API utama untuk ini adalah hook useTransition.


import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';

function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  const [isPending, startTransition] = useTransition();

  const handleNextClick = () => {
    startTransition(() => {
      setUserId(id => id + 1);
    });
  };

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Pengguna Berikutnya
      </button>

      {isPending && <span> Memuat profil baru...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Memuat profil awal...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Inilah yang terjadi sekarang:

  1. Profil awal untuk userId: 1 dimuat, menampilkan fallback Suspense.
  2. Pengguna mengklik "Pengguna Berikutnya".
  3. Panggilan setUserId dibungkus dalam startTransition.
  4. React mulai merender UserProfile dengan userId baru yaitu 2 di memori. Ini menyebabkannya melakukan suspend.
  5. Yang terpenting, alih-alih menampilkan fallback Suspense, React tetap menampilkan UI lama (profil untuk pengguna 1) di layar.
  6. Boolean isPending yang dikembalikan oleh useTransition menjadi true, memungkinkan kita untuk menampilkan indikator pemuatan sebaris yang halus tanpa melepas konten lama.
  7. Setelah data untuk pengguna 2 diambil dan UserProfile dapat dirender dengan sukses, React melakukan pembaruan, dan profil baru muncul dengan mulus.

Transisi menyediakan lapisan kontrol terakhir, memungkinkan Anda membangun pengalaman memuat yang canggih dan ramah pengguna yang tidak pernah terasa mengganggu.

Praktik Terbaik dan Pertimbangan Global

Kesimpulan

React Suspense mewakili lebih dari sekadar fitur baru; ini adalah evolusi fundamental dalam cara kita mendekati asinkronisitas dalam aplikasi React. Dengan beralih dari bendera pemuatan manual yang imperatif dan merangkul model deklaratif, kita dapat menulis komponen yang lebih bersih, lebih tangguh, dan lebih mudah untuk disusun.

Dengan menggabungkan <Suspense> untuk status tertunda, Batas Error untuk status kegagalan, dan useTransition untuk pembaruan yang mulus, Anda memiliki perangkat yang lengkap dan kuat. Anda dapat mengatur segalanya mulai dari spinner pemuatan sederhana hingga tampilan dasbor bertahap yang kompleks dengan kode yang minimal dan dapat diprediksi. Saat Anda mulai mengintegrasikan Suspense ke dalam proyek Anda, Anda akan menemukan bahwa itu tidak hanya meningkatkan kinerja aplikasi dan pengalaman pengguna Anda tetapi juga secara dramatis menyederhanakan logika manajemen state Anda, memungkinkan Anda untuk fokus pada hal yang benar-benar penting: membangun fitur-fitur hebat.